package net.asrsmid.invoker.as;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import org.olap4j.CellSet;
import org.olap4j.CellSetAxis;
import org.olap4j.Position;
import org.olap4j.metadata.Member;

/**
 * public class TableFactory <br/><br/>
 * The class TableFactory helps to construct a {@link Table} object from a 
 * given olap4j cell set.
 * @author Chen WANG
 * @since 19 June 2011
 */
public class TableFactory {
	/**
	 * Constructs a Table object with the given olap4j cell set and the indicated precision
	 * @param cellSet a data set results from an olap4j query
	 * @return a Table object containing the multidimensional data and headers
	 */
	public static Table build(CellSet cellSet) {
		return build(cellSet, 2);
	}
	
	/**
	 * Constructs a Table object with the given olap4j cell set and the indicated precision
	 * @param cellSet a data set results from an olap4j query
	 * @param precision the precision to use when setting the table data (between 0 and 5)
	 * @return a Table object containing the multidimensional data and headers
	 */
	public static Table build(CellSet cellSet, int precision) {
		// TODO : empty cellset
		/* 
		 * a valid precision is between 0 and 5. If the given precision exceeds the bounds, 
		 * then the default precision 2 will be used 
		 */
		precision = Math.abs(precision);
		if (precision > Table.MAX_DATA_PRECISION) precision = Table.MAX_DATA_PRECISION;
		
		// fields to initialize
		Table table = null;
		int[] dimensions = null;
		Object[] headers = null;
		Object data = null;
		
		try {			
			// initialize dimensions and data
			List<CellSetAxis> axes = cellSet.getAxes();
			List<List<Position>> dimPositions = new ArrayList<List<Position>>();
			dimensions = new int[axes.size()];
			
			for (int i = 0; i < dimensions.length; i++) {
				List<Position> positions = axes.get(i).getPositions();
				dimensions[Math.abs(i - dimensions.length + 1)] = positions.size();
				dimPositions.add(positions);
			}
			data = Array.newInstance(Object.class, dimensions);
			
			// load data from cell set
			String err = "(error)";
			/*
			 * Get all possible coordinates in the table's bounds. 
			 * Each coordinates corresponds to a value in the table
			 */
			int[][] allCoords = getAllCoords(dimensions);
			
			// for each coordinates, set the corresponding value
			for (int i = 0; i < allCoords.length; i++) {
				int[] coords = allCoords[i];
				// get the corresponding Positions of a coords to get value from the cell set
				Vector<Integer> vecCoords = new Vector<Integer>();
				Position[] posCoords = new Position[coords.length];
				for (int j = 0; j < coords.length; j++) {
					posCoords[j] = dimPositions.get(j).get(coords[j]);
					vecCoords.add(coords[j]);
				}
				Object value = cellSet.getCell(posCoords).getValue();
				
				// set value to table's data object
				if (value == null || "".equals(value)) {
					// save null if there on value at this coordinates
					setArrayData(data, null, vecCoords);
				} else {
					try {
						// if is numeric, save it in double
						Number numValue = (Number) value;
						java.math.BigDecimal dVal = new java.math.BigDecimal(numValue.doubleValue());
						double roundedVal = dVal.setScale(precision, java.math.BigDecimal.ROUND_HALF_UP).doubleValue(); 
						setArrayData(data, roundedVal, vecCoords);
					} catch (Exception e) {
						// if is other type (Eg. #1.INF) save an error msg
						setArrayData(data, err, vecCoords);
						//e.printStackTrace();
					}
				}
			}
			
			// load headers
			headers = new Object[dimensions.length];
			for (int i = dimPositions.size() - 1; i >= 0; i--) {
				// get positions in order PAGES, ROWS, COLUMNS
				List<Position> positions = dimPositions.get(i);
				int headersLength = positions.size();
				int headersWidth = positions.get(0).getMembers().size();
				// a dimHeader is an array of 2 dim
				Object headerArray = Array.newInstance(String.class, new int[] {headersWidth, headersLength});
				headers[dimensions.length - i - 1] = headerArray;
				
				// load position label
				for (int j = 0; j < positions.size(); j++) {
					Position pos = positions.get(j);
					List<Member> members = pos.getMembers();
					for (int k = 0; k < members.size(); k++) {
						Object val = members.get(k).getCaption();
						Object subHeaderArray = Array.get(headerArray, k);
						Array.set(subHeaderArray, j, val != null ? val.toString() : "");
					}
				}
			}
			
			table = new Table(dimensions, headers, data, precision);
		} catch (Throwable t) {
			t.printStackTrace();
		}
		
		return table;
	}

	/**
	 * Sets a value in the data object ()multidimensional array at the indicated coordinates
	 * @param array table's data field or it's sub array
	 * @param value the value to set
	 * @param coords target coordinates (in order of COLUMNS, ROWS, PAGES ...)
	 * eg. coords = [2, 4, 3] --> column 2, row 4, page 3
	 */
	private static void setArrayData(Object array, Object value, Vector<Integer> coords) {
		if (coords.size() == 1) {
			/*
			 * if coords' length is 1 (one-dimension table), get the array with the ROW coordinate and set the value
			 * in the array at the position indicated by the first value of coords 
			 */
			Array.set(array, coords.get(coords.size() - 1), value);
			// remove the last element with which a sub array has been fetched
			coords.remove(coords.size() - 1);
		} else if (coords.size() == 2) {
			/*
			 * if coords' length is 2 , get the array with the ROW coordinate and set the value
			 * in the array at the position indicated by the first value of coords 
			 */
			Object subArray = Array.get(array, coords.get(coords.size() - 1));
			Array.set(subArray, coords.get(0), value);
		} else {
			/*
			 * if coords' length > 2, invoke it self in a recursively 
			 */
			Object subArray = Array.get(array, coords.get(coords.size() - 1));
			// remove the last element with which a sub array has been fetched
			coords.remove(coords.size() - 1);
			setArrayData(subArray, value, coords);
		}
	}

	/**
	 * Calculate all possible 0-based coordinates for a table's dimensions. 
	 * @param dimensions an int array whose length is table's dimension and indicating the 
	 * length for each dimension
	 * @return an int array of 2 dimensions containing all coordinates 
	 */
	private static int[][] getAllCoords(int[] dimensions) {
		/* find the number of all possible coords */
		int coordsNum = 1;
		int tDim = dimensions.length;
		for (int i = 0; i < tDim; i++) {
			coordsNum = coordsNum * dimensions[i];
		}
		// each coordinates consists a certain (table's dimension) of values
		int[][] allCoords = new int[coordsNum][tDim];
		
		for (int i = 0; i < tDim; i++) {
			/*
			 * get the length this dimension, in order of  COLUMNS, ROWS, PAGES, SECTIONS ...
			 */
			int dimLength = dimensions[tDim - i - 1];
			/*
			 * calculate the repeat times for each single coordinate.
			 * Eg. for a table of [2,2,4] all the possible coordinates are:
			 * C R P
			 * 0 0 0
			 * 0 0 1
			 * 0 0 2
			 * 0 0 3
			 * 0 1 0
			 * 0 1 1
			 * 0 1 2
			 * 0 1 3
			 * 1 0 0
			 * 1 0 1
			 * 1 0 2
			 * 1 0 3
			 * 1 1 0
			 * 1 1 1
			 * 1 1 2
			 * 1 1 3
			 * Of all the 16 coordinates (2 * 2 * 4), each single coord of the PAGES dimension
			 * repeats 1 time while it repeats 4 time for the ROWS dimension and 8 (4 * 2) times
			 * for the COLUMNS dimension
			 * The repeat times = the product of the length of the dimensions which are higher then 
			 * the current one.
			 * Eg. the repeate times for a coord in the PAGES = ROWS's lengt * COLUMNS's length,
			 * for a coord in the ROWS = COLUMNS's length
			 * for a coord in the COLUMNS = 1
			 *  
			 */
			int repeat = 1;
			for (int j = tDim - i - 1; j < tDim; j++) {
				if (j + 1 < tDim)
					repeat = repeat * dimensions[j + 1];
			}
			// compose a list containing all possible coordinates (0-based) at the current dimension 
			List<Integer> dimCoordList = new ArrayList<Integer>();
			for (int n = 0; n < dimLength; n++) {
				dimCoordList.add(n);
			}
			// prepare circle linked list
			CircleLinkedList<Integer> circleDimCoordList = new CircleLinkedList<Integer>(dimCoordList, repeat);
			for (int j = 0; j < allCoords.length; j++) {
				// generate the coordinates at the current dimension with a certain repeat times
				allCoords[j][i] = circleDimCoordList.rotate();
			}
		}
		
		return allCoords;
	}
}